﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace IronWinter
{
    public class Unit
    {
        #region Variables
        public int unitDesignate;
        public int unitType;
        public static Texture2D[] Portraits = new Texture2D[3];
        public static List<UnitSpecs> unitSpecs = new List<UnitSpecs>(); 
        public const int PORTRAIT_WIDTH = 150;
        public const int PORTRAIT_HEIGHT = 180;
        //A const that measures how close you need to be to a point for it to count as "right on".
        public const int BASICALLY_THERE = 1;

        public enum UnitType { Light, Medium, Heavy };
        
        // Default stats. 
        //booleans
        protected bool m_selected;


        // numeral
        protected int m_maxHealth, m_currHealth, m_armor, m_atkPow, m_armorPen, m_value;
        protected float m_moveSpeed;


        protected UnitType type;

        public string name;

        protected AnimatedSprite sprite;
        protected Rectangle dimensions;
        protected Vector2 position;

        public int controlGroup;
        public List<Tuple<Ability, int>> tempEffects;
        public List<Object> waypoints; //Must be Vectors, Buildings, or Units
  
        //Path is the path from the unit's last waypoint to the next waypoint.
        public List<Vector2> path;
        //The size of the unit in game, in terms of nav mesh units. UNITS ARE SQUARE. PERIOD.
        public int size;


        public int range;

        public List<int> abilities;
        public Weapon.types weapon;
        public Armor.types ar;
        public Item.types item;
        public int portrait;

        public TimeSpan atkSpd;
        public TimeSpan atkLast;
        public TimeSpan[] abilityLast;

        public Player player;
        #endregion

        #region GetSet
        //Get and Set methods 

        public int designate
        {
            get { return unitDesignate; }
            set { unitDesignate = value; }
        }
        public int classType
        {
            get { return unitType; }

        }
        public bool alive
        {
            get { return (m_currHealth > 0); }
        }

        public bool isSelected
        {
            get { return m_selected; }
            set
            {
                m_selected = value;
                if (value)
                    sprite.tint = Color.Green;
                else
                    sprite.tint = player.color;
            }
        }


        // numeral
        public int health
        {
            get { return m_currHealth; }
            set { m_currHealth = value; }
        }

        public int maxHealth
        {
            get { return m_maxHealth; }
            set { m_maxHealth = value; }
        }

        public int armor
        {
            get { return m_armor; }
            set { m_armor = value; }

        }

        public int atkPow
        {
            get { return m_atkPow; }
            set { m_atkPow = value; }
        }

        public int armorPen
        {
            get { return m_armorPen; }
            set { m_armorPen = value; }

        }
        public int value
        {
            get { return m_value; }
            set { m_value = value; }
        }

        public float moveSpeed
        {
            get { return m_moveSpeed; }
            set { m_moveSpeed = value; }

        }
        // Get set for Equipment
        

        public Rectangle Bounds
        {
            get { return new Rectangle((int)position.X, (int)position.Y, Map.cellSize * size, Map.cellSize * size); }
        }

        public Vector2 Center
        {
            get { return new Vector2(X + Map.cellSize * size / 2, Y + Map.cellSize * size / 2); }
        }

        public float X
        {
            get { return position.X; }
            set { position.X = value; }
        }

        public float Y
        {
            get { return position.Y; }
            set { position.Y = value; }
        }

        public Vector2 Position
        {
            get { return position; }
            set { position = value; }
        }
        #endregion

        #region Methods

        public Unit(UnitSpecs template, Vector2 location, Player pl)
        {
            unitDesignate = pl.requestUnitID();
            range = 0;
            player = pl;
            position = location;

            waypoints = new List<Object>();
            
            tempEffects = new List<Tuple<Ability, int>>();

            weapon = template.weapon;
            ar = template.ar;
            item = template.item;
            type = template.type;

            //THIS IS ONLY FOR TESTING PURPOSES
            switch (type)
            {
                case UnitType.Light:
                    unitType = 1;
                    m_maxHealth = 200;
                    m_armor = 0;
                    m_atkPow = 9;
                    m_armorPen = 0;
                    m_moveSpeed = 6;
                    m_value = 60;
                    portrait = 2;
                    sprite = new AnimatedSprite(Game1.lightUnit, 0, 0, Map.cellSize, Map.cellSize,1);
                    break;
                case UnitType.Medium:
                    unitType = 2;
                    m_maxHealth = 300;
                    m_armor = 2;
                    m_atkPow = 9;
                    m_armorPen = 0;
                    m_moveSpeed = 4;
                    m_value = 60;
                    portrait = 1;
                    sprite = new AnimatedSprite(Game1.mediumUnit, 0, 0, Map.cellSize, Map.cellSize, 1);
                    break;
                case UnitType.Heavy:
                    unitType = 3;
                    m_maxHealth = 400;
                    m_armor = 3;
                    m_atkPow = 9;
                    m_armorPen = 0;
                    m_moveSpeed = 2;
                    m_value = 60;
                    portrait = 0;
                    sprite = new AnimatedSprite(Game1.heavyUnit, 0, 0, Map.cellSize, Map.cellSize, 1);
                    break;
            }

            sprite.tint = player.color;

            applyGear(EquipmentLibrary.getWeapon(weapon));
            applyGear(EquipmentLibrary.getArmor(ar));
            applyGear(EquipmentLibrary.getItem(item));


            //Units have to have a move speed of at least 2 or they will not move (why?)
            if (m_moveSpeed < 1)
                m_moveSpeed = 1;

            m_currHealth = m_maxHealth;
            abilities = new List<int>();
            abilities.AddRange(template.abilities);

            atkSpd = TimeSpan.FromSeconds(EquipmentLibrary.getWeapon(weapon).rate);
            abilityLast = new TimeSpan[3];
            abilityLast.Initialize();

            name = template.name;
            //Yeah, I'm testing this too.
            size = 1;

        }

        public void remake()
        {
            size = 1; 
            //THIS IS ONLY FOR TESTING PURPOSES
            switch (type)
            {
                case UnitType.Light:
                    portrait = 2;
                    if (sprite == null)
                    {
                        sprite = new AnimatedSprite(Game1.lightUnit, 0, 0, Map.cellSize, Map.cellSize, 1);
                    }
                    break;
                case UnitType.Medium:
                    portrait = 1;
                    if (sprite == null)
                    {
                        sprite = new AnimatedSprite(Game1.mediumUnit, 0, 0, Map.cellSize, Map.cellSize, 1);
                    }
                    break;
                case UnitType.Heavy:
                    portrait = 0;
                    if (sprite == null)
                    {
                        sprite = new AnimatedSprite(Game1.heavyUnit, 0, 0, Map.cellSize, Map.cellSize, 1);
                    }
                    break;
            }
            sprite.tint = player.color;
        }

        public Unit()
        {
            waypoints = new List<Object>();
            tempEffects = new List<Tuple<Ability, int>>();
        }

        public void applyGear(Equipment i)
        {

            m_maxHealth += i.maxHealth;
            m_armor += i.armor;
            m_atkPow += i.atkPow;
            m_armorPen += i.armorPen;
            m_moveSpeed += i.moveSpeed;
            range += i.range;

            m_value += i.value;
        }

        public void immediateAttack(Unit target)
        {
            waypoints.Clear();
            if (path != null)
                path.Clear();
            waypoints.Add(target);
        }
        public void immediateAttack(Building target)
        {
            waypoints.Clear();
            if (path != null)
                path.Clear();
            waypoints.Add(target);
        }

        public void appendAttack(Unit target)
        {
            waypoints.Add(target);
        }
        public void appendAttack(Building target)
        {
            waypoints.Add(target);
        }

        public void takeDamage(int dmg, int penetration)
        {
            if (dmg < 0) //healing
            {
                m_currHealth -= dmg;
                if (m_currHealth > m_maxHealth)
                    m_currHealth = m_maxHealth;
            }
            else
            {
                if (penetration < m_armor && (dmg - (m_armor - penetration) > 0))
                    m_currHealth -= (dmg - (m_armor - penetration));
                else
                    m_currHealth -= dmg;
            }
                
        }

        public void takeDamage(Unit attacker)
        {
            takeDamage(attacker.atkPow, attacker.armorPen);

            if (waypoints == null || waypoints.Count == 0)
            {
                appendAttack(attacker);
            }
        }

        //when the an ability is applied, apply the initial damage then add buffs/curses, then add to list. 

        public void applyAbility(Ability abl)
        {
            takeDamage(abl.damage, abl.targetArmorPen);

            m_maxHealth += abl.maxHealth;
            m_armor += abl.armor;
            m_atkPow += abl.atkPow;
            m_armorPen += abl.targetArmorPen;
            m_moveSpeed += abl.moveSpeed;

            tempEffects.Add((new Tuple<Ability,int>(abl, abl.duration))); //can change this duration to a calculation of end time for simpler use
        }

        // When the durration of an ability wears off remove all player status effects and remove the ability from the list
        public void removeAblility(Tuple<Ability, int> i)
        {
            Ability abl = i.Item1;

            m_maxHealth += abl.maxHealth;
            m_armor += abl.armor;
            m_atkPow += abl.atkPow;
            m_armorPen += abl.targetArmorPen;
            m_moveSpeed += abl.moveSpeed;

            tempEffects.Remove(i);
        }

        public void Update(GameTime gameTime)
        {
            //Can replace this with a simpler form, calculate end time of buffs at activation,
            //then check if end has passed 
            foreach (Tuple<Ability, int> i in tempEffects)
            {
                if (i.Item2 - 1 <= 0)
                {
                    removeAblility(i);
                }
                else
                {
                    tempEffects.Remove(i);
                    tempEffects.Add(new Tuple<Ability, int>(i.Item1, i.Item2 - 1));
                }
                
            }
            if (waypoints == null || waypoints.Count == 0)
            {
                //This would be a good place to move units away from eachother, provided that only untasked (no waypoints) units are moved.
                Collision.siftAgainstUnits(this);
                idleAttack(gameTime);
                return;
                //(will implement "Idle" method)
            }
            //For Attacking
            //Check to see if Unit is in range
            if(waypoints[0] is Unit)
            {
                Unit uTarget = (Unit)waypoints[0];
                if (!uTarget.alive)
                {
                    waypoints.Remove(uTarget);
                    //We're going to need to create a new path to go to the next waypoint.
                    path = null;
                }
                else
                {
                    if (inRangeOf(uTarget))
                    {
                        //Attack speed now limited
                        if (atkLast + atkSpd <= gameTime.TotalGameTime)
                        {
                            //uTarget.takeDamage(this);
                            atkLast = gameTime.TotalGameTime;
                            //sprite.tint = sprite.tint == Color.Bisque ? Color.DarkOrchid : Color.Bisque; //test
                            Game1.bullets.Add(new Bullet(this, uTarget,EquipmentLibrary.getWeapon(weapon).color));
                        }
                        //Go ahead and sift units since they're still in range.
                        //Do it here so the unit gets at least one attack before it's pushed away.
                        Collision.siftAgainstUnits(this);
                        return;
                    }
                }

                
            }

            if (waypoints == null || waypoints.Count == 0)
            {
                idleAttack(gameTime);
                return;
            }

            // check to see if Building is in range.
            if (waypoints[0] is Building)
            {
                Building bTarget = (Building)waypoints[0];
                if (!bTarget.alive)
                {
                    waypoints.Remove(bTarget);
                    //We're going to need to create a new path to go to the next waypoint.
                    path = null;
                }
                else
                {
                    if (inRangeOf(bTarget))
                    {
                        if (atkLast + atkSpd <= gameTime.TotalGameTime)
                        {
                            //bTarget.takeDamage(m_atkPow, m_armorPen);
                            atkLast = gameTime.TotalGameTime;
                            //sprite.tint = sprite.tint == Color.Bisque ? Color.DarkOrchid : Color.Bisque; //test
                            Game1.bullets.Add(new Bullet(this, bTarget, EquipmentLibrary.getWeapon(weapon).color));
                        }
                        //Go ahead and sift units since they're still in range.
                        //Do it here so the unit gets at least one attack before it's pushed away.
                        Collision.siftAgainstUnits(this);
                        return;
                    }
                }


            }

            if (waypoints == null || waypoints.Count == 0)
            {
                idleAttack(gameTime);
                return;
            }


            //If the unit has a vector2 waypoint, go ahead and try moving.
            if (waypoints[0] is Vector2)
            {
                // update movement. LAST
                pathFind();
                // actually move. SUPER-CALIFRAGILISTICEXPIALIDOCIOUS LAST
                pathFollow();
            }//If the Unit has a Unit or Building waypoint, continue moving only if the Unit is off of attack cooldown.
            else if (waypoints[0] is Building || waypoints[0] is Unit)
            {
                if (atkLast + atkSpd <= gameTime.TotalGameTime)
                {
                    // update movement. LAST
                    pathFind();
                    // actually move. SUPER-CALIFRAGILISTICEXPIALIDOCIOUS LAST
                    pathFollow();
                }
                else
                {
                    Collision.siftAgainstUnits(this);
                }
            }
            else
            {
                //The above are the only things that can be in a waypoint list.
                Console.WriteLine("I just don't know what went wrong~.");
            }
        }

        private void idleAttack(GameTime gameTime)
        {
            foreach (Player p in Game1.players)
            {
                if (p == player)
                    continue;

                foreach (Unit u in p.units)
                {
                    if (inRangeOf(u))
                    {
                        //Attack speed now limited
                        if (atkLast + atkSpd <= gameTime.TotalGameTime)
                        {
                            //u.takeDamage(this); //bullets do damage now, not units
                            atkLast = gameTime.TotalGameTime;
                            //sprite.tint = sprite.tint == Color.Bisque ? Color.DarkOrchid : Color.Bisque; //test
                            Game1.bullets.Add(new Bullet(this, u, EquipmentLibrary.getWeapon(weapon).color));
                        }
                        return;
                    }
                }

                foreach (Terrain t in Game1.currentMap.SpriteTerrain)
                {
                    if (!(t is HQBuilding))
                        continue;

                    if (((HQBuilding)t).ctrlPlayer == player)
                        continue;

                    
                        //Attack speed now limited
                    if (inRangeOf((Building)t))
                    {
                        if (atkLast + atkSpd <= gameTime.TotalGameTime)
                        {
                            //u.takeDamage(this); //bullets do damage now, not units
                            atkLast = gameTime.TotalGameTime;
                            //sprite.tint = sprite.tint == Color.Bisque ? Color.DarkOrchid : Color.Bisque; //test
                            Game1.bullets.Add(new Bullet(this, (Building)t, EquipmentLibrary.getWeapon(weapon).color));
                        }
                        return;
                    }
                }
            }
        }

        private Boolean inRangeOf(Vector2 v)
        {
            float xDistance = position.X - v.X;
            float yDistance = position.Y - v.Y;

            return (range * range > xDistance * xDistance + yDistance * yDistance);
        }

        //Works with centers
        private Boolean inRangeOf(Unit u)
        {
            float xDistance = Center.X - u.Center.X;
            float yDistance = Center.Y - u.Center.Y;
            float totalSquared = xDistance * xDistance + yDistance * yDistance;
            float maxDist = range + u.size * Map.cellSize / 2;
            maxDist *= maxDist;

            return (maxDist > totalSquared);
        }

        private Boolean inRangeOf(Building b)
        {
            float xDistance = Center.X - b.Center.X;
            float yDistance = Center.Y - b.Center.Y;
            float totalSquared = xDistance * xDistance + yDistance * yDistance;
            float maxDist = range + b.size * Map.cellSize / 2;
            maxDist *= maxDist;

            return (maxDist > totalSquared);
        }

        private void pathFind()
        {
            Vector2 nextPosition;
            //Path should be null if there are no points left in it.
            if (path == null || path.Count == 0)
                path = null;
            //If there are no waypoints, there's nothing to do here.
            if ((waypoints == null) || waypoints.Count == 0)
            {
                return;
            }

            else if (waypoints[0] is Unit)
            {
                Unit u = (Unit)waypoints[0];
                nextPosition = new Vector2(u.X, u.Y);
            }

            else if (waypoints[0] is Building)
            {
                Building b = (Building)waypoints[0];
                nextPosition = new Vector2(b.X, b.Y);
            }

            else if (waypoints[0] is Vector2)
            {
                nextPosition = (Vector2)waypoints[0];
            }

            else if (waypoints[0] is Point)
            {
                Point p = (Point)waypoints[0];
                nextPosition = new Vector2(p.X, p.Y);
            }
            else
            {
                return;
                // Curse because this should never happen
            }

            if (Collision.outsideMap(nextPosition))
            {
                removeFirstWaypoint();
                return;
            }

            //If nextPosition is nasty and collides with stuff, fix it.
            if (Collision.floatLocationCollides(this, nextPosition, Map.cellSize, Collision.world))
            {
                nextPosition = Collision.closestValidCoord(nextPosition, Map.cellSize, this, Collision.world);
            }

            float deltaX = Math.Abs(position.X - nextPosition.X);
            float deltaY = Math.Abs(position.Y - nextPosition.Y);
            
            //This waypoint is useless if we've already reached it.
            if ((deltaX <= BASICALLY_THERE) && (deltaY <= BASICALLY_THERE) && (waypoints[0] is Vector2 || waypoints[0] is Point))
            {
                removeFirstWaypoint();
                return;
            }

            //If there is currently no path to follow, try to create one.
            bool canGoThere = true;
            if (path == null)
            {
                canGoThere = Collision.findPath(this, nextPosition);
            }
            else { return; }//If the path is not null, then we already have something to do.

            //If you can't reach the destination, remmove it from the waypoint list.
            if (!canGoThere)
            {
                waypoints.RemoveAt(0);
                if (waypoints.Count == 0)
                    return;
                pathFind();
                return;
            }
        }

        //Does everything you need to do when you remove a waypoint from the waypoints list.
        //THIS FUNCTION SHOULD TYPICALLY BE FOLLOWED BY A RETURN CALL because it is typically called from pathFind, and it calls pathFind itself.
        private void removeFirstWaypoint()
        {
            waypoints.RemoveAt(0);
            //We're going to need to create a new path to go to the next waypoint.
            path = null;
            //Actually, only if there are waypoints left.
            if (waypoints.Count == 0)
            {
                return;
            }
            pathFind();
        }

        private void pathFollow()
        {
            //If there are no waypoints, there's nothing to do here.
            if ((waypoints == null) || waypoints.Count == 0)
                return;
            //Otherwise continue as usual.
            float leftToMove = m_moveSpeed;
            //For each iteration, move up to the remaining "leftToMove".
            while (true)
            {
                Vector2 nextCoords = path[0];
                float dx = nextCoords.X - position.X;
                float dy = nextCoords.Y - position.Y;
                //If you are BASICALLY THERE, don't worry about this node in the path.
                while (Math.Abs(dx) < BASICALLY_THERE && Math.Abs(dy) < BASICALLY_THERE)
                {
                    path.RemoveAt(0);
                    //The above line could conceivably empty out the path.
                    if (path.Count == 0)
                        return;
                    nextCoords = path[0];
                    dx = nextCoords.X - position.X;
                    dy = nextCoords.Y - position.Y;
                } 
                //If the entire path is done, or leftToMove is basically zero, break.
                //I'm aware this means the unit can't traverse multiple waypoints in one frame.
                if (path.Count == 0 || leftToMove < 0.5)
                {
                    break;
                }
                //If it so happens that we can't reach the destination without using more than our remaining move, then use up the rest and break.
                if (leftToMove * leftToMove < dx * dx + dy * dy)
                {
                    //Move the distance of leftToMove, proprtional to the values of dx and dy.
                    position.X += leftToMove * (dx / (Math.Abs(dx) + Math.Abs(dy)));
                    position.Y += leftToMove * (dy / (Math.Abs(dx) + Math.Abs(dy)));
                    //After moving, you've used up the remainder of your move, so do cleanup and return.
                    break;
                }
                else//Otherwise we're going to need to only move a portion of our leftToMove value, determined by dx and dy.
                {
                    position.X += dx;
                    position.Y += dy;
                    leftToMove -= (float)Math.Sqrt(dx * dx + dy * dy);
                }
            }
        }



        // (add and clear waypoints)
        public void appendWaypoint(Vector2 newWaypoint)
        {
            waypoints.Add(newWaypoint - new Vector2(size * Map.cellSize / 2, size * Map.cellSize / 2));
        }

        public void setWaypoint(Vector2 newWaypoint)
        {
            waypoints.Clear();
            if (path != null)
                path.Clear();
            appendWaypoint(newWaypoint);
        }

        public void Draw(SpriteBatch sb)
        {
            sprite.Draw(sb, Bounds,sprite.tint);
            float percentHealth = (float)m_currHealth / (float)m_maxHealth;
            sb.Draw(Game1.genericTexture, new Rectangle((int)Position.X, (int)Position.Y - 6, (int)(Map.cellSize * size), 4), Color.MistyRose);
            sb.Draw(Game1.genericTexture, new Rectangle((int)Position.X, (int)Position.Y - 6, (int)(percentHealth * Map.cellSize * size), 4),Color.LightGreen);
        }
    }
        #endregion

    [Serializable]
    public class UnitSpecs : IComparable
    {
        public Unit.UnitType type = Unit.UnitType.Light;
        public Boolean isActive = false;
        private Weapon.types m_weapon;
        private Armor.types m_ar;
        private Item.types m_item;
        public int cost;

        private List<int> m_abilities;

        public string name = "New Unit";

        public Weapon.types weapon
        {
            get { return m_weapon; }
            set
            {
                m_weapon = value;
                UpdateCost();
            }
        }

        public Armor.types ar
        {
            get { return m_ar; }
            set
            {
                m_ar = value;
                UpdateCost();
            }
        }

        public Item.types item
        {
            get { return m_item; }
            set
            {
                m_item = value;
                UpdateCost();
            }
        }

        public List<int> abilities
        {
            get { return m_abilities; }
            set
            {
                m_abilities = value;
                UpdateCost();
            } 
        }
        
        public UnitSpecs() 
        {
            m_abilities = new List<int>();
            UpdateCost();   
        }

        public UnitSpecs(UnitSpecs us) : this()
        {
            type = us.type;
            m_weapon = us.weapon;
            m_ar = us.ar;
            m_item = us.item;
            m_abilities = new List<int>();
            m_abilities.AddRange(us.abilities);
            name = us.name;
            
        }

        public void UpdateCost()
        {
            cost = EquipmentLibrary.getArmor(ar).value + EquipmentLibrary.getWeapon(weapon).value + EquipmentLibrary.getItem(item).value;

            foreach (int i in m_abilities)
                cost += Ability.AbilityList[i].value;
        }

        public int CompareTo(Object obj)
        {
            UnitSpecs us = this;
            try
            {
                us = (UnitSpecs)obj;
            }
            catch
            {
                return 0;
            }

            if ((int)this.type < (int)us.type)
                return -1;
            else if ((int)this.type > (int)us.type)
                return 1;
            else if (this.name.CompareTo(us.name) < 0)
                return -1;
            else if (this.name.CompareTo(us.name) > 0)
                return 1;
            else
                return 0;
        }
    }
}
